Skip to content

[compiler] WIP port of React Compiler to Rust#36173

Open
josephsavona wants to merge 293 commits intofacebook:mainfrom
josephsavona:rust-research
Open

[compiler] WIP port of React Compiler to Rust#36173
josephsavona wants to merge 293 commits intofacebook:mainfrom
josephsavona:rust-research

Conversation

@josephsavona
Copy link
Copy Markdown
Member

@josephsavona josephsavona commented Mar 30, 2026

This is an experimental, work-in-progress port of React Compiler to Rust. Key points:

  • Work-in-progress - we are sharing early, prior to testing internally at Meta, to get feedback from partners in parallel with continued development.
  • No builds available yet, you'll have to do some hacking if you want to try this.
  • Most fixture tests pass, but there are known gaps and potentially lurking bugs.
  • The architecture was heavily guided by humans (me, @josephsavona) but majority coded by AI. I was very hands-on in setting the architecture, the testing and verification strategy, incremental migration approach, etc. I also kept a close eye on the code and spent a decent amount of time going back and forth to get code quality to a decent level.
  • The rough API is "Rust Babel AST" + Scope Info in, Rust Babel AST out. We use a Rust representation of the Babel AST as our "public API", as it were, and then each integration (Babel, OXC, SWC) converts to/from their native representation. For now integrations must also provide scope information - in the future React Compiler may compute bindings and references itself from the AST.
  • The port is very much pass-by-pass, maintaining the same algorithms, approaches, etc.
  • Early performance numbers are derived from AI and i haven't spent much time validating the benchmark setup, beyond the fact that the optimization opportunities it discovered made complete sense and the fixes were right. With that caveat, itt does appear that the Rust version is quite fast already: 3x faster when operating as a Babel plugin. The serialization cost is quite high, but the actual transformation logic is ~10x faster, so it's net faster. Native integrations (oxc, swc) should be even faster.
  • There are 3 integrations right now: an alternative Babel plugin (which will eventually get removed as we integrate into babel-plugin-react-compiler), and examples of what OXC and SWC integrations could look like (see react_compiler_oxc and react_compiler_swc crates).

correctness:

  • 1717/1718 tests pass in snap using the temporary alternate Babel plugin. the failing fixture is a test for ValidateSourceLocations, and isn't critical for correctness
  • The OXC and SWC example integrations currently have significant correctness issues, i have an agent working on them

development:

  • yarn snap --rust is the primary test suite, testing that we error or compile as expected. It does not test the inner state of the compiler along the way, though, making it less suitable for finding subtle logic gaps btw the TS and Rust versions. It's also Babel based, making it less easy to test OXC and SWC integrations.
  • compiler/scripts/test-e2e.sh is an e2e test of all 3 variants (babel wrapper around Rust, OXC/SWC integrations) against the TS implementation. This does a partial comparison, focused on final output code only (doesn't test error details etc). Useful for getting the swc and oxc integrations closer to parity.
  • compiler/script/test-rust-port.sh does detailed testing of the internal compiler state after each pass, in addition to checking the final output code. This is the key script used to port the compiler, ensuring not just that the output was the same but that each pass was capturing all the same detail. This script can be pointed at any directory of JS files, which we expect to use for internal testing at Meta.

Joe Savona added 30 commits March 16, 2026 09:36
…ering, and JSX

M9: ArrowFunctionExpression, FunctionExpression, FunctionDeclaration, ObjectMethod lowering with recursive lower_inner(). gather_captured_context walks scope_info references. capture_scopes helper. lower_function_to_value, lower_function_declaration, lower_object_method. HirBuilder.scope_info_and_env_mut() for disjoint borrow.
M10: JSXElement, JSXFragment, lower_jsx_element_name (builtin/identifier/member), lower_jsx_element (text/element/fragment/expression/spread children), trim_jsx_text whitespace normalization, JSX attribute handling.
… chaining, remaining statements

SwitchStatement with case discrimination and fallthrough. TryStatement with handler blocks and enter_try_catch. ForOfStatement with GetIterator/IteratorNext protocol. ForInStatement with NextPropertyOf. OptionalCallExpression and OptionalMemberExpression with optional chaining control flow. lower_reorderable_expression and is_reorderable_expression. WithStatement error, ClassDeclaration todo, export declarations, remaining type declarations.
Implements lower_assignment() for all pattern types: Identifier (StoreLocal/StoreContext/StoreGlobal), MemberExpression (PropertyStore/ComputedStore), ArrayPattern (Destructure with items, holes, spreads, nested followups), ObjectPattern (Destructure with properties, rest, nested followups), AssignmentPattern (default values with undefined check using Ternary/Branch control flow), RestElement. Also simplifies VariableDeclaration to delegate to lower_assignment for all patterns.
Replaces all remaining todo!() stubs with proper error handling: delete unary operator (PropertyDelete/ComputedDelete), throw unary (unsupported error), logical assignment operators (todo error), compound member assignment (todo error), AssignmentPattern in expression position (todo error), Pipeline operator (panic), export declarations (skip in function bodies), lower_type (returns Poly).
All milestones implemented. No todo!() stubs remain.
…rovements

Fixes from thorough review against plan and TS source: lowerValueToTemporary optimization (skip extra instruction for unnamed temporaries), VariableDeclaration now handles var-kind errors, no-init declarations (DeclareLocal/DeclareContext), destructure-style detection. TypeCast expressions now emit TypeCastExpression instruction instead of silently unwrapping. Environment gains fn_type field for ReactFunctionType classification (Component/Hook/Other). ObjectMethod getter/setter error check. is_reorderable_expression now handles TS/Flow type wrapper expressions.
…compound member assignment, enum errors

Implements UpdateExpression with MemberExpression arguments (read value, compute increment/decrement, store back via PropertyStore/ComputedStore). Implements compound assignment (+=, -= etc.) to MemberExpression targets (read, compute binary op, store back). Enum declarations now emit UnsupportedNode with error instead of silently skipping. Adds TS/Flow type wrapper handling to is_reorderable_expression.
Add DebugLogEntry type to CompileResult for returning debug log entries
(kind: "debug") from Rust to JS. The compile_program function now logs
the EnvironmentConfig, matching the TS compiler's behavior. The JS shim
forwards debug logs to logger.debugLogIRs when available.
Distill key architectural constraints from rust-port-research.md and
rust-port-notes.md into a concise reference covering arenas, ID types,
error handling, pass structure, side maps, and structural similarity.
Wire the Babel plugin entrypoint (program.rs) to the HIR lowering pipeline.
Add FunctionNode enum to pass discovered functions directly to lower(),
removing the redundant extract_function traversal from build_hir.rs. Create
entrypoint/pipeline.rs (analogous to TS Pipeline.ts) that orchestrates
Environment creation and BuildHIR. Functions are now lowered through the
full entrypoint path with debug HIR output in compile results.
Add compiler-review agent and skill for reviewing Rust port code against
the original TypeScript source. Update existing skills/rules/agents to
reference rust-port-architecture.md instead of rust-port-notes.md and
rust-port-research.md. Add compiler-review as a step in /compiler-commit.
Unify error types by removing local CompileError and TryCompileResult enums
in favor of CompilerError from react_compiler_diagnostics. Add CodegenFunction
placeholder, flow debug logs via callback instead of return value, and apply
panicThreshold logic in process_fn's error path.
Add a new DebugPrintHIR.ts with an exhaustive debug printer for HIR that
prints all fields of every type (Identifier, Place, InstructionValue,
Terminal, AliasingEffect, ReactiveScope, Type, etc.) with first-seen
deduplication for identifiers and scopes. This enables pass-by-pass
comparison of TS and Rust compiler output to verify port correctness.
Rewrite debug_print.rs to use a DebugPrinter struct with indent/dedent,
first-seen identifier/scope deduplication, inline type expansion, and
Environment/Errors section matching the TS output format.
…ecture

Make ProgramContext mutable and accumulate events/debug_logs directly on it
instead of returning them from process_fn. Add CompilerOutputMode enum derived
from PluginOptions, thread it through compile_program → process_fn →
try_compile_function → compile_fn → Environment. Change process_fn to return
Option<CodegenFunction> matching TS, collect compiled functions for future AST
rewriting, and align handle_error/log_error to take &mut ProgramContext.
Replace the standalone-binary test approach with a unified TS script
(test-rust-port.ts) that runs both TS and Rust compilers through their
real Babel plugins, captures debug log entries via the logger API, and
diffs output for a specific pass. Update pipeline.rs to use
DebugLogEntry::new() (kind: "debug") for pre-formatted string output.
The shell wrapper now builds the native module and delegates to the TS
script.
Error with a helpful message if the TypeScript compiler produces no log
entries for the given pass name across all fixtures, indicating the pass
name is likely incorrect.
Move the cargo build + dylib copy into the TS script so the native
module is always rebuilt before testing, avoiding stale binary issues.
The shell wrapper is now a simple passthrough to the TS script.
Capture CompileError, CompileSkip, CompileUnexpectedThrow, and
PipelineError events from both compilers via logEvent and diff them
alongside debug entries. Also throw a TODO if a reactive/ast log entry
matches the target pass, so unsupported kinds surface immediately.
… BuildHIR/HIRBuilder

Fix multiple port fidelity issues identified by review against the TypeScript
source. Key changes: implement BlockStatement hoisting logic with DeclareContext
emission, add fbt/fbs depth tracking for JSX whitespace handling, fix
import/export error reporting, handle pipeline operator gracefully instead of
panicking, fix JSXNamespacedName to produce Primitive string (matching TS),
add JSX attribute colon validation, fix ClassDeclaration error category,
add MemberExpression Reassign invariant check, add context variable kind
validation, handle \r\n line endings in trim_jsx_text, and add hoisted
identifier tracking to Environment.
Create skill and agent for automating the TS-to-Rust pass porting workflow.
The skill orchestrates context gathering, planning, implementation via subagent,
test-fix loop, and review. Also makes lowering crate helper functions public
so optimization passes can reuse them.
Point to rust-port-architecture.md for translation guidelines and data
modeling rules instead of inlining a translation table.
…5→772 tests passing)

Fix multiple issues in the Rust HIR lowering to improve test-rust-port HIR pass
results from 55 to 772 passing out of 1717 fixtures:

- Add Display traits for Effect, BlockKind, BinaryOperator, UnaryOperator,
  LogicalOperator, UpdateOperator, ObjectPropertyType to match TS formatting
- Fix scope.ts to register program scope explicitly (program.traverse doesn't
  visit the Program node) so local bindings aren't misidentified as module-level
- Map constantViolations (assignment LHS, update exprs, for-of/for-in) in scope
  extraction so reassigned variables are properly resolved
- Propagate source locations through resolve_identifier to set identifier locs
- Pre-allocate 28 built-in type slots to match TS global type counter offset
- Skip prefilter when compilationMode is 'all' in BabelPlugin.ts
- Remove inner function printing from debug_hir (TS prints inline)
- Add ID normalization in test-rust-port.ts for opaque counter differences
…tion id, and debug print (772→951 tests passing)

Fix source locations throughout HIR lowering to match TypeScript behavior:
use sub-node locs (consequent, body, left-side, identifier) instead of
parent statement locs for if/for/while/do-while/for-in/for-of/labeled
statements, assignment expressions, compound member assignments, and
update expressions. Extract type annotations from Babel AST JSON for
DeclareLocal/StoreLocal. Use the AST function's own id (null for arrow
functions) instead of the inferred name. Fix UpdateOperator debug format
to output ++/-- instead of Increment/Decrement.
…n error diffing

Merge entries and events into a single ordered log, stopping capture once the
target pass is reached. CompileError events now include severity, category, and
all diagnostic detail objects (error locs/messages and hints) for exact matching
between TS and Rust. The EnvironmentConfig debug entry is skipped since its
formatting differs between implementations.
…rs, and name dedup (1190→1467 tests passing)

Fix assignment expression lowering to return temporary results matching TS behavior,
use correct locs for logical expressions and return-without-value, add context identifier
pre-computation (findContextIdentifiers equivalent) via ScopeInfo, share used_names
across function scope boundaries for name deduplication, remove incorrect promote_temporary
for AssignmentPattern, and handle member expression assignments inline.
…nstead of JS

Add a generic AST visitor (visitor.rs) with scope tracking, and use it to
implement FindContextIdentifiers in Rust (find_context_identifiers.rs).
Remove referenceToScope and reassignments from the serialized scope info —
context identifiers are now computed entirely from the AST and scope tree.
Create react_compiler_optimization crate and port PruneMaybeThrows and
MergeConsecutiveBlocks from TypeScript. Wire PruneMaybeThrows into the
Rust pipeline after lowering. Update test-rust-port.ts to handle passes
that appear multiple times in the TS pipeline via stride-based entry
matching (774/780 HIR-passing fixtures also pass PruneMaybeThrows).
Creates a new react_compiler_validation crate with ports of both validation passes. Adds NonLocalBinding::name() helper to react_compiler_hir and wires both passes into pipeline.rs after PruneMaybeThrows. Validation invariant errors are suppressed until lowering is complete.
…edFunctionExpressions

Port two early pipeline passes to Rust: dropManualMemoization removes
useMemo/useCallback calls (replacing with direct invocations/references
and optionally inserting StartMemoize/FinishMemoize markers), and
inlineImmediatelyInvokedFunctionExpressions inlines IIFEs into the
enclosing function's CFG with single-return and multi-return paths.
Also adds standalone mergeConsecutiveBlocks call after IIFE inlining
to match TS pipeline order.
Joe Savona added 18 commits March 29, 2026 14:09
…alues

The TS uses Map<DeclarationId, ReactiveInstruction> but the instruction
ref is only used for JS-style mutation. Rust's two-phase approach
doesn't need the value side, so HashSet is the idiomatic equivalent.
…lMemoization

PruneHoistedContexts: remove redundant insert before remove in
StoreContext Function branch.

ValidatePreservedManualMemoization: add missing StartMemoize nesting
invariant, FinishMemoize id matching, fix record_temporaries ordering,
remove incorrect LoadLocal/LoadContext entries in record_deps_in_value,
add missing invariant in validate_inferred_dep.
…nal debug_print

Add timing instrumentation across the Rust compiler pipeline and JS/Rust
bridge, controlled by a `__profiling` flag in plugin options. Add a standalone
profiling script (profile-rust-port.ts) that compares TS vs Rust compiler
performance with fine-grained per-pass timing. Make debug_print calls
conditional on a `__debug` flag, eliminating ~71% of overhead when no logger
is configured — reducing the Rust/TS ratio from 3.16x to ~1.2x.
Change CompileResult.ast from Option<serde_json::Value> to
Option<Box<RawValue>>, serializing the AST directly to a JSON string
in compile_program rather than going through an intermediate Value.
This avoids a full extra serialization pass (File→Value→String becomes
File→String) when the NAPI layer serializes the final result.
Stop cloning every debug entry into both debug_logs and ordered_log.
Remove the debug_logs field from CompileResult and ProgramContext entirely,
using ordered_log as the single source of truth. The JS side already
prefers ordered_log when available, so this is a no-op for consumers.
This halves the serialization cost for debug data.
Remove the debugLogs optional field from CompileSuccess and CompileError
interfaces in bridge.ts, and remove the dead fallback code path in
BabelPlugin.ts that read from debugLogs. orderedLog is now the single
source for debug entries.
…void per-function EnvironmentConfig clone

Replace regex compilation in find_program_suppressions with simple
string matching (strip_prefix + starts_with), eliminating ~240ms of
per-fixture regex compilation overhead. Also hoist the EnvironmentConfig
clone from try_compile_function (called per function) to compile_program
(called once per file), reducing allocation overhead.
…gistry

Replace HashMap type aliases with newtype structs supporting a base+overlay
pattern. Built-in shapes and globals are initialized once via LazyLock and
shared across all Environments. Custom hooks and module types go into per-
environment overlay maps. Cloning for outlined functions now copies only the
small extras map. ~18% overall speedup across 1717 fixtures.
- Migrate react_compiler_oxc convert_ast.rs to OXC v0.121 API (~341 errors fixed)
- Wire up OXC transform() to actually call the compiler
- Fix RawValue deserialization in both SWC and OXC frontends
- Fix SWC convert_ast_reverse and emit to produce correct output
- Update e2e CLI to use new TransformResult.file field
Add a --rust flag to the snap test runner so fixtures can be run through
the Rust compiler backend (babel-plugin-react-compiler-rust) for
side-by-side comparison. Includes buildRust() for cargo build + native
module copy, watch mode support for .rs file changes, TS type fixes in
the Rust babel plugin, and removal of debug eprintln! lines from pipeline.rs.
Format CompilerDiagnostic/CompilerErrorDetail class instances into plain
objects before passing to logEvent(). Both TS and Rust now emit the same
flat structure: {category, reason, description, severity, suggestions,
details/loc} with no nested `options` wrapper or getter-based properties.
Also adds index/filename to source locations in logger events from Rust,
and propagates source locations to codegen AST nodes for blank line handling.
- Add source locations to JSX elements, text nodes, identifiers, and
  namespaced names in the Rust codegen, matching the TS codegen's
  withLoc pattern
- Switch BabelPlugin from direct AST assignment to prog.replaceWith()
  so subsequent Babel plugins (fbt, idx) properly traverse the new AST
- Add re-entry guard for replaceWith re-traversal
- Add formatCompilerError for proper error message formatting
- Fix source_filename in error info construction

Snap: 841→1605/1718 passing (+764).
Propagate identifierName from Babel AST source locations through HIR
diagnostics and into logger event serialization. This field appears on
CompilerDiagnosticDetail::Error locations for identifier-related errors
(e.g., validateUseMemo, validateNoRefAccessInRender).

Snap: 1605→1606/1718.
…, identifierName

Fix 38 snap test failures across several categories:

- Fix FBT plugin crashes by ensuring JSX attribute value nodes have loc (19 tests)
- Fix preserve-memo error descriptions to include inferred/source dependency details (18 tests)
- Fix categoryToHeading: UnsupportedSyntax maps to 'Compilation Skipped' not 'Todo'
- Add identifierName to diagnostics from validate_use_memo and validate_no_set_state_in_effects
- Add loc to codegen primitive values for downstream plugin compatibility

Snap tests: 1644/1718 passed (was 1606), 74 failed. Pass-level: 1717/1717.
…r formatting

- Add identifier_name extraction in validate_no_derived_computations_in_effects
  and validate_no_set_state_in_effects using source code byte offsets
- Add identifier_name_for_id helper to Environment
- Fix BabelPlugin error formatting: scope extraction errors, raw exception messages
- Add raw_message field to CompilerErrorInfo for unknown exception passthrough

Snap: 1644→1661/1718 (+17).
…egen

- Add "Inferred dependencies" hint text to validateExhaustiveDependencies,
  matching TS output format for dependency mismatch errors
- Fix invariant error formatting in codegen: separate reason from message
  in MethodCall and unnamed temporary errors
- Add logged_severity() to CompilerDiagnostic for PreserveManualMemo
- Fix code frame message in validate_no_derived_computations_in_effects

Snap: 1661→1672/1718 (+11).
… and prefilter

Fix snap test failures (46→16 remaining) across multiple categories:
validation error suppression (has_invalid_deps on StartMemoize), type
provider and knownIncompatible checks, JSON log field ordering and
severity, code-frame abbreviation, codegen error formatting and loc
propagation, React.memo/forwardRef prefilter detection, toString()
return type inference, ref-like name detection, and component-syntax
ref parameter binding resolution. Also auto-enables sync mode for
yarn snap --rust.
Auto-formatted Options.ts, Program.ts, runner-watch.ts, and
runner-worker.ts via yarn prettier-all.
@meta-cla meta-cla bot added the CLA Signed label Mar 30, 2026
@github-actions github-actions bot added the React Core Team Opened by a member of the React Core Team label Mar 30, 2026
Joe Savona added 3 commits March 30, 2026 14:53
FBT loc propagation on identifier/place/declarator/JSXAttribute nodes,
component/hook declaration syntax via __componentDeclaration and
__hookDeclaration AST fields, 13 missing BuiltInMixedReadonly methods,
identifierName in effect-derived-computation diagnostics, and
ValidateSourceLocations skip. Only remaining failure is the intentional
error.todo-missing-source-locations (pass not ported to Rust).
Comprehensive comparison of TypeScript and Rust compiler implementations
covering all major subsystems, generated from systematic review.
…dNode codegen

Fix two critical gaps identified in the Rust port gap analysis:

1. Transitive freeze: Added recursive freeze_function_captures_transitive()
   that freezes through arbitrarily nested FunctionExpression captures,
   matching the TS freezeValue → freeze → freezeValue recursion chain.
   Added test fixture that verifies the fix (previously failed in Rust).

2. UnsupportedNode expression codegen: Expression-level handler now attempts
   to deserialize the original AST node from JSON (like the statement-level
   handler) instead of emitting a broken __unsupported_ placeholder.

Updated gap analysis doc to remove fixed items and annotate Gap 5 (early
return) with investigation findings — simply removing it causes downstream
panics, requiring a more careful fix.
… use Err returns

Per the architecture guide, invariant and todo errors should return
Err(CompilerDiagnostic) instead of recording on env. Fixed all error sites
in infer_mutation_aliasing_effects (invariant uninitialized access,
known_incompatible throws, todo spread syntax) to return Err, matching the
TS behavior where these are CompilerError.invariant()/throwTodo()/throw
calls that abort compilation. This allowed removing the pipeline's early
return guard after inferMutationAliasingEffects since the ? operator now
naturally short-circuits on these errors.
@ubugeeei

This comment was marked as spam.

@ubugeeei

This comment was marked as spam.

@xamgore

This comment was marked as spam.

@ericadalton124-stack

This comment was marked as spam.

find_functions_to_compile now delegates to visit_statement_for_functions,
which recursively walks into block-containing statements (if, try, for,
while, switch, labeled, etc.) to find functions at any depth — matching
the TS compiler's Babel traverse behavior. In 'all' mode, recursion is
skipped since the TS scope check only compiles program-scope functions.
Also updated replace_fn_in_statement and rename_identifier_in_statement
to recurse similarly so compiled output is applied correctly.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed React Core Team Opened by a member of the React Core Team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants